#include "sweSolver.hpp"

#include "bc_base_all.hpp"
#include "bc_swe_all.hpp"

#include "exceptions.hpp"
#include "magic_enum.hpp"

namespace hms
{

template<typename MeshType>
void SWESolver<MeshType>::updateBoundary(
	Field<MeshType>& field,
	unsigned patchIndex
){
	const BoundaryPatch& patch { this->mesh().boundaryPatches()[patchIndex] };
	if ( !patch.empty() ){
		BoundaryCondition<MeshType>* bc =
			field.boundaryConditions()[patchIndex].get();
		assert(bc);
		bc->update();
	}
}

/** @brief Updates the ghost values of all fields according to their boundary
 * conditions */
template<typename MeshType>
void SWESolver<MeshType>::updateBoundariesImp(){
	// OMP_PRAGMA( parallel for collapse(2) )
	for ( Field<MeshType>& field : this->m_fields){
		OMP_PRAGMA( parallel for )
		for ( unsigned i = 0; i<this->mesh().boundaryPatches().size(); ++i ){
			updateBoundary(field, i);
		}
	}
}




template<typename MeshType>
void verifyConstantWaterDepth(
	const SWESolver<MeshType>& solver,
	const BoundaryPatch& patch
){
	using F = typename SolverBase<SWESolver<MeshType>>::FieldName;
	/* water depth must have constant value equal to dGhost */
	scalar dGhost {signalCode::uninitialised};
	if ( auto bc = dynamic_cast<const ConstantWaterDepth_velocity<MeshType>*>(
		&( solver.field(F::Velocity).boundaryCondition(patch) )
	) )
		dGhost = bc->dGhost();
	else
		assert(false && "Only call this function with correct bc type!");
	
	auto ghostValues { solver.field(F::WaterDepth).ghostValues(patch) };
	if ( !ghostValues.isConstant(dGhost) ){
		throw BoundaryConditionError(
			"Patch \"" + patch.name() + "\" error: "
			"When using the constant water depth boundary condition, "
			"water depth must have uniform value equal to the one "
			"specified in the velocity boundary condition!"
		);
	}
}


template<typename MeshType>
void verifyOrthogonalFlowVelocity(
	const SWESolver<MeshType>& solver,
	const BoundaryPatch& patch
){
	using F = typename SolverBase<SWESolver<MeshType>>::FieldName;
	scalar vBoundary_velocity {0}, vBoundary_waterDepth {0};

	if ( auto bc = dynamic_cast<const OrthogonalFlowVelocity_velocity<MeshType>*>(
		&( solver.field(F::Velocity).boundaryCondition(patch) )
	) )
		vBoundary_velocity = bc->vBoundary();
	else
		assert(false && "Only call this function with correct bc type!");

	if ( auto bc = dynamic_cast<const OrthogonalFlowVelocity_waterdepth<MeshType>*>(
		&( solver.field(F::WaterDepth).boundaryCondition(patch) )
	) )
		vBoundary_waterDepth = bc->vBoundary();
	else
		assert(false && "Only call this function with correct bc type!");

	if ( !approxEqual(vBoundary_velocity, vBoundary_waterDepth) ){
		throw BoundaryConditionError(
			fmt::format(
				"{0} Patch \"{1}\": Orthogonal flow velocity specified in Fields "
				"\"WaterDepth\" and \"Velocity\" do not match:"
				"\n{2}in {4:<{3}} vBoundary = {5}"
				"\n{2}in {6:<{3}} vBoundary = {7}",
				bold(  "Boundary velocity mismatch!" ), // 0
				bold( "{}", patch.name() ), // 1
				indent(), 13, // 2, 3
				"\"WaterDepth\":", bold(yellow("{}", vBoundary_waterDepth)), // 4, 5
				"\"Velocity\":"  , bold(yellow("{}", vBoundary_velocity  ))  // 6, 7
			)
		);
	}
}


template<typename MeshType>
void verifyWeirBC(
	const SWESolver<MeshType>& solver,
	const BoundaryPatch& patch
){
	scalar
		alpha_d {-1},
		alpha_v {-1},
		alpha_z {-1},
		cp_d {0},
		cp_v {0},
		cp_z {0};

	if ( auto bc = dynamic_cast<const Weir_velocity<MeshType>*>(
		&( solver.v().boundaryCondition(patch) )
	) ){
		alpha_v = bc->alpha();
		cp_v = bc->cp();
	} else
		assert(false);

	if ( auto bc = dynamic_cast<const Weir_waterdepth<MeshType>*>(
		&( solver.d().boundaryCondition(patch) )
	) ){
		alpha_d = bc->alpha();
		cp_d = bc->cp();
	} else
		assert(false);

	if ( auto bc = dynamic_cast<const Weir_bottomElevation<MeshType>*>(
		&( solver.z().boundaryCondition(patch) )
	) ){
		alpha_z = bc->alpha();
		cp_z = bc->cp();
	} else
		assert(false);

	auto throwMismatch = [&](
		std::string_view varName,
		std::string_view unit,
		scalar var_d,
		scalar var_v,
		scalar var_z
	){
		auto varFmt = [&](std::string_view fieldName, scalar var){
			return fmt::format(
				"in {0:<{1}} {2} = {4} [{3}]",
				fmt::format( "\"{}\":", fieldName), 18,
				varName, unit,
				bold(yellow("{0:< 7.4g}", var))
			);
		};
		throw BoundaryConditionError( fmt::format(
			"Weir: Mismatched parameter {0}! Patch \"{1}\":"
			"\n{2}{3}"
			"\n{2}{4}"
			"\n{2}{5}",
			bold( "{}", varName ), // 0
			bold( "{}", patch.name() ), // 1
			indent(), // 2
			varFmt("WaterDepth"     , var_d), // 3
			varFmt("Velocity"       , var_v), // 4
			varFmt("BottomElevation", var_z)  // 5
		) );
	};

	if ( !approxEqual( alpha_d, alpha_v, alpha_z ) )
		throwMismatch("alpha", "rad", alpha_d, alpha_v, alpha_z);

	if ( !approxEqual( cp_d, cp_v, cp_z ) )
		throwMismatch("Cp", "-", cp_d, cp_v, cp_z);
}


template<typename MeshType>
void SWESolver<MeshType>::verifyMatchingBoundaryConditions(
	const BoundaryPatch& patch
) const
{
	using T = boundaryCondition::Type;
	using magic_enum::enum_name;

	auto getType = [&](FieldName f){
		return this->field(f).boundaryCondition(patch).type();
	};

	FieldName
		fieldName,
		velocity   {FieldName::Velocity},
		waterDepth {FieldName::WaterDepth},
		bottomElev {FieldName::BottomElevation};

	T bcType;

	auto checkBCMatch = [&]( FieldName otherField, T otherRequired ){
		T otherType { getType(otherField) };
		if (otherRequired != otherType){
			throw BoundaryConditionError( fmt::format(
				"{0} Field \"{2}\", patch \"{1}\" "
				"has boundary condition type \"{4}\". "
				"In Field \"{3}\", the same patch must then have "
				"boundary condition type \"{5}\", "
				"but instead type \"{6}\" was found!",
				bold( "Boundary condition mismatch!" ), // 0
				patch.name(), // 1
				enum_name(fieldName), // 2
				enum_name(otherField), // 3
				enum_name(bcType), // 4
				bold(green( "{}", enum_name(otherRequired) )),
				bold(red  ( "{}", enum_name(otherType) ))
			) );
		}
	};

	/* for velocity field, check water depth field and vice versa */
	fieldName = velocity;
	bcType = getType(fieldName);

	switch ( bcType ){
		case T::slip_velocity:
			checkBCMatch( waterDepth, T::zeroGradient );
			break;
		case T::constantWaterDepth_velocity:
			checkBCMatch( waterDepth, T::staticValue );
			verifyConstantWaterDepth(*this, patch);
			break;
		case T::freeOutflow_velocity:
			checkBCMatch( waterDepth, T::freeOutflow_waterdepth );
			break;
		case T::orthogonalDischarge_velocity:
			checkBCMatch( waterDepth, T::orthogonalDischarge_waterdepth );
			break;
		case T::orthogonalFlowVelocity_velocity:
			checkBCMatch( waterDepth, T::orthogonalFlowVelocity_waterdepth );
			verifyOrthogonalFlowVelocity(*this, patch);
			break;
		case T::weir_velocity:
			checkBCMatch( waterDepth, T::weir_waterdepth );
			checkBCMatch( bottomElev, T::weir_bottomElevation );
			verifyWeirBC(*this, patch);
			break;
		default:
			;
	}

	/* This is necessary to prevent using an SWE specific water depth BC
	 * in conjunction with a basic BC for velocity. */
	/* for water depth, only the type of the velocity bc must be checked,
	 * because if the type is correct, the other checks have been executed
	 * already, and if the type is not correct, throwing the incorrect type
	 * error is sufficient */
	fieldName = waterDepth;
	bcType = getType(fieldName);

	switch ( bcType ){
		case T::freeOutflow_waterdepth:
			checkBCMatch( velocity, T::freeOutflow_velocity );
			break;
		case T::orthogonalDischarge_waterdepth:
			checkBCMatch( velocity, T::orthogonalDischarge_velocity );
			break;
		case T::orthogonalFlowVelocity_waterdepth:
			checkBCMatch( velocity, T::orthogonalFlowVelocity_velocity );
			break;
		default:
			;
	}
}

template class SWESolver<Mesh>;
template class SWESolver<UniMesh>;
template class SWESolver<RectMesh>;
template class SWESolver<StructMesh>;

} // namespace hms
